﻿using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Linq.Expressions;

namespace Mbs.Reflection
{
    public class DynamicMethodFactory:IDynamicMethodFactory
    {
        #region Helper methods

        private static readonly ConstructorInfo TargetParameterCountExceptionConstructor =
          typeof(TargetParameterCountException).GetConstructor(Type.EmptyTypes);
        private static readonly Module Module = typeof(DynamicMethodFactory).Module;


        private static DynamicMethod CreateDynamicFunc()
        {
            return new DynamicMethod(String.Empty, typeof(object), new[] { typeof(object), typeof(object[]) }, Module, true);
        }

        private static DynamicMethod CreateDynamicProc()
        {
            return new DynamicMethod(String.Empty, typeof(void), new[] { typeof(object), typeof(object[]) }, Module, true);
        }

        private static void EmitDynamicMethod(MethodInfo method,DynamicMethod callable)
        {
            var info = new DelegateBuildInfo(method);

            ILGenerator il = callable.GetILGenerator();

            EmitCheckParameters(info, il, 1);
            EmitLoadParameters(info, il, 1);

            if (method.IsStatic)
                il.EmitCall(OpCodes.Call, method, null);
            else if (method.IsVirtual)
                il.EmitCall(OpCodes.Callvirt, method, null);
            else
                il.EmitCall(OpCodes.Call, method, null);

            if (method.ReturnType == typeof(void))
                il.Emit(OpCodes.Ldnull);
            else
            {
                if (method.ReturnType.IsValueType)
                    il.Emit(OpCodes.Box, method.ReturnType);
               
            }
            il.Emit(OpCodes.Ret);
           
        }

        private static DynamicMethod CreateDynamicFactoryMethod()
        {
            return new DynamicMethod(String.Empty, typeof(object), new[] { typeof(object[]) }, Module, true);
        }

        private static DynamicMethod CreateDynamicGetterMethod()
        {
            return new DynamicMethod(String.Empty, typeof(object), new[] { typeof(object) }, Module, true);
        }

        private static DynamicMethod CreateDynamicSetterMethod()
        {
            return new DynamicMethod(String.Empty, typeof(void), new[] { typeof(object), typeof(object) }, Module, true);
        }

        private static void EmitCheckParameters(DelegateBuildInfo info, ILGenerator il, int argumentArrayIndex)
        {
            Label beginLabel = il.DefineLabel();

            EmitLoadArg(il, argumentArrayIndex);
            il.Emit(OpCodes.Ldlen);
            EmitLoadInt(il, info.Parameters.Length);
            il.Emit(OpCodes.Beq, beginLabel);

            il.Emit(OpCodes.Newobj, TargetParameterCountExceptionConstructor);
            il.Emit(OpCodes.Throw);

            il.MarkLabel(beginLabel);
        }

        private static void EmitLoadParameters(DelegateBuildInfo info, ILGenerator il, int argumentArrayIndex)
        {
            if (!info.Method.IsStatic && !(info.Method is ConstructorInfo))
            {
                il.Emit(OpCodes.Ldarg_0);
                EmitUnboxOrCast(il, info.Method.DeclaringType);
            }

            for (int index = 0; index < info.Parameters.Length; index++)
            {
                EmitLoadArg(il, argumentArrayIndex);
                EmitLoadInt(il, index);
                il.Emit(OpCodes.Ldelem_Ref);
                EmitUnboxOrCast(il, info.ParameterTypes[index]);
            }
        }

        private static Type[] GetActualParameterTypes(ParameterInfo[] parameters)
        {
            Type[] types = new Type[parameters.Length];

            for (int index = 0; index < parameters.Length; index++)
            {
                Type type = parameters[index].ParameterType;
                types[index] = (type.IsByRef ? type.GetElementType() : type);
            }

            return types;
        }

        private static void EmitUnboxOrCast(ILGenerator il, Type type)
        {
            if (type.IsValueType)
                il.Emit(OpCodes.Unbox_Any, type);
            else
                il.Emit(OpCodes.Castclass, type);
        }

        private static void EmitLoadInt(ILGenerator il, int value)
        {
            switch (value)
            {
                case -1:
                    il.Emit(OpCodes.Ldc_I4_M1);
                    break;
                case 0:
                    il.Emit(OpCodes.Ldc_I4_0);
                    break;
                case 1:
                    il.Emit(OpCodes.Ldc_I4_1);
                    break;
                case 2:
                    il.Emit(OpCodes.Ldc_I4_2);
                    break;
                case 3:
                    il.Emit(OpCodes.Ldc_I4_3);
                    break;
                case 4:
                    il.Emit(OpCodes.Ldc_I4_4);
                    break;
                case 5:
                    il.Emit(OpCodes.Ldc_I4_5);
                    break;
                case 6:
                    il.Emit(OpCodes.Ldc_I4_6);
                    break;
                case 7:
                    il.Emit(OpCodes.Ldc_I4_7);
                    break;
                case 8:
                    il.Emit(OpCodes.Ldc_I4_8);
                    break;
                default:
                    if (value > -129 && value < 128)
                        il.Emit(OpCodes.Ldc_I4_S, (sbyte)value);
                    else
                        il.Emit(OpCodes.Ldc_I4, value);
                    break;
            }
        }

        private static void EmitLoadArg(ILGenerator il, int index)
        {
            switch (index)
            {
                case 0:
                    il.Emit(OpCodes.Ldarg_0);
                    break;
                case 1:
                    il.Emit(OpCodes.Ldarg_1);
                    break;
                case 2:
                    il.Emit(OpCodes.Ldarg_2);
                    break;
                case 3:
                    il.Emit(OpCodes.Ldarg_3);
                    break;
                default:
                    if (index > -129 && index < 128)
                        il.Emit(OpCodes.Ldarg_S, (sbyte)index);
                    else
                        il.Emit(OpCodes.Ldarg, index);
                    break;
            }
        }
        #endregion

      
        public Func CreateFuncMethod(MethodInfo method)
        {
            var dm = CreateDynamicFunc();
            EmitDynamicMethod(method, dm);

            return dm.CreateDelegate(typeof(Func)) as Func;
        }

       
        public Proc CreateProcMethod(MethodInfo method)
        {
            Proc result = (target,args) => CreateFuncMethod(method)(target,args);
            return result;
        }

        public ConstructorHandler CreateConstructorMethod(ConstructorInfo constructor)
        {
            DynamicMethod callable = CreateDynamicFactoryMethod();
            var info = new DelegateBuildInfo(constructor);

            Type returnType = constructor.ReflectedType;
            ILGenerator il = callable.GetILGenerator();

            EmitCheckParameters(info, il, 0);
            EmitLoadParameters(info, il, 0);

            il.Emit(OpCodes.Newobj, constructor);

            if (info.ReturnType.IsValueType)
                il.Emit(OpCodes.Box, returnType);

            il.Emit(OpCodes.Ret);

            return callable.CreateDelegate(typeof(ConstructorHandler)) as ConstructorHandler;
        }


        public Getter CreateGetter(FieldInfo field)
        {
            DynamicMethod callable = CreateDynamicGetterMethod();

            Type returnType = field.FieldType;
            ILGenerator il = callable.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            EmitUnboxOrCast(il, field.DeclaringType);
            il.Emit(OpCodes.Ldfld, field);

            if (returnType.IsValueType)
                il.Emit(OpCodes.Box, returnType);

            il.Emit(OpCodes.Ret);

            return callable.CreateDelegate(typeof(Getter)) as Getter;
        }


        public Getter CreateGetter(PropertyInfo property)
        {
            DynamicMethod callable = CreateDynamicGetterMethod();

            Type returnType = property.PropertyType;
            ILGenerator il = callable.GetILGenerator();
            MethodInfo method = property.GetGetMethod();

            il.Emit(OpCodes.Ldarg_0);
            EmitUnboxOrCast(il, property.DeclaringType);

            if (method.IsFinal)
                il.Emit(OpCodes.Call, method);
            else
                il.Emit(OpCodes.Callvirt, method);

            if (returnType.IsValueType)
                il.Emit(OpCodes.Box, returnType);

            il.Emit(OpCodes.Ret);

            return callable.CreateDelegate(typeof(Getter)) as Getter;
        }

        public static Getter CreateGetter(MethodInfo method)
        {
            ParameterExpression instanceParameter = Expression.Parameter(typeof(object), "target");

            MethodCallExpression call = Expression.Call(
                Expression.Convert(instanceParameter, method.DeclaringType),
                method);

            Expression<Getter> lambda = Expression.Lambda<Getter>(
                Expression.Convert(call, typeof(object)),
                instanceParameter);

            return lambda.Compile();
        }

        public Setter CreateSetter(FieldInfo field)
        {
            DynamicMethod callable = CreateDynamicSetterMethod();

            Type returnType = field.FieldType;
            ILGenerator il = callable.GetILGenerator();

            il.DeclareLocal(returnType);

            il.Emit(OpCodes.Ldarg_1);
            EmitUnboxOrCast(il, returnType);
            il.Emit(OpCodes.Stloc_0);

            il.Emit(OpCodes.Ldarg_0);
            EmitUnboxOrCast(il, field.DeclaringType);
            il.Emit(OpCodes.Ldloc_0);

            il.Emit(OpCodes.Stfld, field);
            il.Emit(OpCodes.Ret);

            return callable.CreateDelegate(typeof(Setter)) as Setter;
        }


        public Setter CreateSetter(PropertyInfo property)
        {
            DynamicMethod callable = CreateDynamicSetterMethod();

            Type returnType = property.PropertyType;
            ILGenerator il = callable.GetILGenerator();
            MethodInfo method = property.GetSetMethod();

            il.DeclareLocal(returnType);

            il.Emit(OpCodes.Ldarg_1);
            EmitUnboxOrCast(il, returnType);
            il.Emit(OpCodes.Stloc_0);

            il.Emit(OpCodes.Ldarg_0);
            EmitUnboxOrCast(il, property.DeclaringType);
            il.Emit(OpCodes.Ldloc_0);

            if (method.IsFinal)
                il.Emit(OpCodes.Call, method);
            else
                il.Emit(OpCodes.Callvirt, method);

            il.Emit(OpCodes.Ret);

            return callable.CreateDelegate(typeof(Setter)) as Setter;
        }

        class DelegateBuildInfo
        {
            public MethodBase Method { get; private set; }
            public Type ReturnType { get; private set; }
            public ParameterInfo[] Parameters { get; private set; }
            public Type[] ParameterTypes { get; private set; }

            public DelegateBuildInfo(ConstructorInfo ctor)
            {
                Method = ctor;
                ReturnType = ctor.ReflectedType;
                InitParameters();
            }

            public DelegateBuildInfo(MethodInfo method)
            {
                Method = method;
                ReturnType = method.ReturnType;
                InitParameters();
            }

            private void InitParameters()
            {
                Parameters = Method.GetParameters();
                ParameterTypes = GetActualParameterTypes(Parameters);
            }
        }
    }
}
